﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: WeakReferenceCache_2.cs
//
//  Description: A simple cache based on weak references.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Threading;

namespace Chili.Opf3
{
	/// <summary>
	/// Implements a simple cache based upon weak references.
	/// </summary>
	internal sealed class WeakReferencesCache<K, V> : IDisposable
	{
		#region CachedItem

		/// <summary>
		/// Represents one item that is cached.
		/// </summary>
		private struct CachedItem<T>
		{
			private WeakReference _wr;
			private object _tag;

			/// <summary>
			/// Creates a new instance of the CacheItem.
			/// </summary>
			/// <param name="target">The object instance that is saved in the item.</param>
			/// <param name="tag">The tag of the object instance.</param>
			public CachedItem(T target, object tag)
			{
				_wr = new WeakReference(target);
				_tag = tag;
			}

			/// <summary>
			/// Returns the target.
			/// </summary>
			public T Target
			{
				get { return (T)_wr.Target; }
			}

			/// <summary>
			/// Returns the tag of the item.
			/// </summary>
			public object Tag
			{
				get { return _tag; }
				set { _tag = value; }
			}

			/// <summary>
			/// Specifies whether the target is alive.
			/// </summary>
			public bool IsAliveTarget
			{
				get { return _wr.IsAlive; }
			}
		}

		#endregion

		private readonly TimeSpan TIMEOUT = TimeSpan.FromMilliseconds(50);
		private const int RUNS_TO_GC = 100;
		private Thread _collector;
		private List<K> _keys = new List<K>();
		private Dictionary<K, List<CachedItem<V>>> _references = new Dictionary<K, List<CachedItem<V>>>();

		/// <summary>
		/// Destroys the current instance of the 
		/// </summary>
		~WeakReferencesCache()
		{
			Dispose();
		}

		/// <summary>
		/// Adds an object to the cache.
		/// </summary>
		/// <param name="key">Key of the object.</param>
		/// <param name="obj">Object that is cached.</param>
		/// <param name="tag">An object that is saved connected to the object.</param>
		public void Add(K key, V obj, object tag)
		{
			WeakReference reference = new WeakReference(obj);

			lock (this)
			{
				// check if the references dictionary contains a list for the given items.
				List<CachedItem<V>> items;
				if (!_references.TryGetValue(key, out items))
				{
					// if not create the list and add it to the references.
					items = new List<CachedItem<V>>();

					_keys.Add(key);
					_references.Add(key, items);
				}

				// add the item to the list of items.
				items.Add(new CachedItem<V>(obj, tag));
	
				// Initialize the cleanup thread. Do it only if there is no thread running.
				if (_collector == null)
				{
					_collector = new Thread(new ThreadStart(CollectionThread));
					// Thread is set background to have it immediately terminated if the environment is shut down.
					_collector.IsBackground = true;
					_collector.Start();
				}
			}
		}

		/// <summary>
		/// Returns the tag associated with the given key.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="obj"></param>
		public object GetObjectTag(K key, V obj)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");

			lock (this)
			{
				// Check if the references list contains the item.
				List<CachedItem<V>> items;
				if (_references.TryGetValue(key, out items))
				{
					// loop over all items that have been returned.
					for (int i = 0; i < items.Count; i++)
					{
						CachedItem<V> item = items[i];

						if (obj.Equals(item.Target))
							return item.Tag;
					}
				}

				return null;
			}
		}

		/// <summary>
		/// Updatges the tag associated with the given key.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="obj"></param>
		/// <param name="tag"></param>
		public void UpdateObjectTag(K key, V obj, object tag)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");

			lock (this)
			{
				// Check if the references list contains the items.
				List<CachedItem<V>> items;
				if (_references.TryGetValue(key, out items))
				{
					// loop over all items that have been returned.
					for (int i = 0; i < items.Count; i++)
					{
						CachedItem<V> item = items[i];

						if (obj.Equals(item.Target))
						{
							item.Tag = tag;
							break;
						}
					}
				}
			}
		}

		/// <summary>
		/// Returns the first object associated with the given key.
		/// </summary>
		/// <param name="key"></param>
		public object GetFirstObjectByKey(K key)
		{
			lock (this)
			{
				// Check if the references list contains the items.
				List<CachedItem<V>> items;
				if (_references.TryGetValue(key, out items))
				{
					if (items.Count == 0 || !items[0].IsAliveTarget)
						return null;
					else
						return items[0].Target;
				}

				return null;
			}
		}

		/// <summary>
		/// Removes the object from the cache.
		/// </summary>
		/// <param name="key">Key of the object.</param>
		public void Remove(K key)
		{
			lock (this)
			{
				_keys.Remove(key);
				_references.Remove(key);
			}
		}

		/// <summary>
		/// Revmoes all the object in the cache.
		/// </summary>
		public void RemoveAll()
		{
			_keys.Clear();
			_references.Clear();
		}

		/// <summary>
		/// Returns whether the cache contains an item with the given key.
		/// </summary>
		/// <param name="key">The key of the item.</param>
		public bool ContainsKey(K key)
		{
			return (this.GetFirstObjectByKey(key) != null);
		}

		#region Garbage Collection

		/// <summary>
		/// Garbage collector thread.
		/// </summary>
		private void CollectionThread()
		{
			try
			{
				int i = 0;
				while (true)
				{
					// Wait a bit.
					Thread.Sleep(TIMEOUT);
					i++;

					// Do the collect in the 100th round.
					if (i >= RUNS_TO_GC)
					{
						// Do collection of disposed references.
						CollectDisposedReferences();
						i = 0;
					}
				}
			}
			catch (ThreadAbortException)
			{
				// Ignore the exception an exit.
			}
		}

		/// <summary>
		/// Collects the disposed references.
		/// </summary>
		private void CollectDisposedReferences()
		{
			if (_references.Count == 0)
				return;

			// Temporary list that contains the keys of the disposed references.
			List<K> disposed = new List<K>();

			// loop over all keys that are stored.
			for (int i = 0; i < _keys.Count; i++)
			{
				// look the class to avoid other people manipulating it.
				lock (this)
				{
					// make sure that nobody else made us go over the end of the list by removing items.
					if (i >= _keys.Count)
						break;

					// get the current key.
					K key = _keys[i];

					// get the list of items that are associated with the key.
					List<CachedItem<V>> items = _references[key];

					// loop over all of them to check how many are alive.
					for (int j = 0; j < items.Count; j++)
					{
						// remove the dead ones.
						if (!items[j].IsAliveTarget)
							items.RemoveAt(j);
					}

					// if no items are remaining in the list, add it to the disposed list.
					if (items.Count == 0)
						disposed.Add(key);
				}
			}

			// loop over all disposed references and keys and remove them.
			foreach (K key in disposed)
			{
				Remove(key);
			}

			// prints the amount of objects that have been disposed.
//			System.Diagnostics.Trace.WriteLine(
//				string.Format("{0} objects disposed.\nSize of references cache: {1}\nSize of keys cache: {2}", 
//				disposed.Count, _references.Count, _keys.Count));
		}

		#endregion

		#region IDisposable Members

		/// <summary>
		/// Disposes this instance.
		/// </summary>
		public void Dispose()
		{
			// Abort the current thead if still running.
			if (_collector != null)
			{
				_collector.Abort();
				_collector = null;
			}

			_keys.Clear();
			_references.Clear();
		}

		#endregion
	}
}
